using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace BoxBinary.AspectCache.Core.Helpers
{
    /// <summary>
    /// Provides helper methods for the serialization and deserialization of objects
    /// </summary>
    public static class Serialization
    {
        /// <summary>
        /// Serializes an object to disk.
        /// </summary>
        /// <param name="storeRootDir">The organisational folder used for many serialized files.</param>
        /// <param name="serializationFilename">The filename of the single file to be serialized.</param>
        /// <param name="itemForSerialization">The object to be serialized.</param>
        /// <param name="fullPathForSerialization">The full path to the file where the object was serialized after this method has returned.</param>
        public static void SerializeToFile(string storeRootDir, string serializationFilename,
                                           object itemForSerialization, out string fullPathForSerialization)
        {
            #region Validate

            if (string.IsNullOrEmpty(storeRootDir))
                throw new ArgumentException("storeRootDir");
            if (string.IsNullOrEmpty(serializationFilename))
                throw new ArgumentException("serializationFilename");
            if (itemForSerialization == null)
                throw new ArgumentNullException("itemForSerialization");

            #endregion

            // First ensure the storeRootDir exists
            if (!Directory.Exists(storeRootDir))
                Directory.CreateDirectory(storeRootDir);

            // Generate the full path for serialization
            fullPathForSerialization = System.IO.Path.Combine(storeRootDir, serializationFilename);

            FileStream fStream = null;
            try
            {
                // If the file already exists, open it, otherwise create it
                if (File.Exists(fullPathForSerialization))
                {
                    // First attempt to remove a readonly flag, for example if Visual Studio
                    // got hold of it and stuck it in VSS...
                    TryRemoveReadOnlyFlag(fullPathForSerialization);

                    // Get a FileStream to the existing file.
                    fStream = IO.OpenFileOrWait(fullPathForSerialization, FileMode.Truncate, FileAccess.Write,
                                                FileShare.Read, TimeSpan.FromSeconds(5));
                }
                else
                {
                    // Get a FileStream of the new file.
                    fStream = IO.OpenFileOrWait(fullPathForSerialization, FileMode.Create, FileAccess.Write,
                                                FileShare.Read, TimeSpan.FromSeconds(5));
                }

                // Ensure FileStream is disposed immediately after use
                using (fStream)
                {
                    // Pass the FileStream on for actual serialization
                    SerializeToFileStream(fStream, itemForSerialization);
                }
            }
            catch
            {
                // If we get here, we've thrown an error, so try and delete the file,
                // to keep things clean
                try
                {
                    if (File.Exists(fullPathForSerialization))
                        File.Delete(fullPathForSerialization);
                }
                catch
                {
                    /* Ignore error */
                }

                // Throw the original error
                throw;
            }
            finally
            {
                if (fStream != null)
                    fStream.Close();
            }
        }

        /// <summary>
        /// Serializes an object to a <see cref="FileStream"/>.
        /// </summary>
        /// <param name="backingStore"></param>
        /// <param name="itemToSerialize"></param>
        public static void SerializeToFileStream(FileStream backingStore, object itemToSerialize)
        {
            #region Validate Parameters

            if (backingStore == null)
                throw new ArgumentNullException("backingStore");
            if (itemToSerialize == null)
                throw new ArgumentNullException("itemToSerialize");

            #endregion

            try
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(backingStore, itemToSerialize);
            }
            catch (SerializationException sEx)
            {
                throw sEx;
            }
            catch (Exception innerException)
            {
                //TODO: Resource this exception
                throw new ApplicationException("Could not serialize file to the FileStream", innerException);
            }
        }

        /// <summary>
        /// Deserializes an object from a FileStream
        /// </summary>
        /// <param name="backingStore"></param>
        /// <returns></returns>
        public static object DeserializeFromFileStream(FileStream backingStore)
        {
            #region Validate Parameters

            if (backingStore == null)
                throw new ArgumentNullException("backingStore");

            #endregion

            try
            {
                var formatter = new BinaryFormatter();
                return formatter.Deserialize(backingStore);
            }
            catch (Exception innerException)
            {
                //TODO: Resource this exception
                throw new ApplicationException(
                    string.Format("Deserialize(FileStream) error '{0}'", innerException.Message), innerException);
            }
        }

        /// <summary>
        /// Deserializes an object from a file.
        /// </summary>
        /// <param name="absoluteFilePath"></param>
        /// <returns></returns>
        public static object DeserializeFromFile(string absoluteFilePath)
        {
            #region Validate Parameters

            if (string.IsNullOrEmpty(absoluteFilePath))
                throw new ArgumentNullException("absoluteFilePath");

            #endregion

            try
            {
                // First check that the file exists
                if (File.Exists(absoluteFilePath))
                {
                    // Some methods may call Deserialize before a file is actually available,
                    // e.g. a FileSystemWatcher responding to a Create event - which happens on or around
                    // when the first byte is actually written. Here we wait a maximum of 5 seconds for the
                    // file to be unlocked.

                    // UPDATED 2007 03 27 by Alex N as there was an opportunity for a file lock to occur between the call
                    // to Helpers.IO.WaitUntilFileAvailable and File.Open. This new function, Helpers.IO.OpenFileOrWait, keeps the lock as soon
                    // as it becomes available and this lock is used for deserializing
                    using (
                        FileStream fs = IO.OpenFileOrWait(absoluteFilePath, FileMode.Open, FileAccess.Read,
                                                          FileShare.Read, TimeSpan.FromSeconds(30)))
                        return DeserializeFromFileStream(fs);
                }
                return null;
            }
            catch (Exception ex)
            {
                //TODO: Resource this exception
                throw new ApplicationException(string.Format("Deserialize(string) error '{0}'", ex.Message), ex);
            }
        }

        /// <summary>
        /// Attempts to remove the readonly flag from a file. If an error occurs, it is ignored.
        /// </summary>
        /// <param name="absolutePathToFile"></param>
        /// <returns></returns>
        public static bool TryRemoveReadOnlyFlag(string absolutePathToFile)
        {
            try
            {
                var fInfo = new FileInfo(absolutePathToFile);

                if (fInfo.Exists && fInfo.IsReadOnly)
                    fInfo.IsReadOnly = false;

                return true;
            }
            catch
            {
                return false;
            }
        }
    }
}